package top.novel.ioc.factory;

import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import top.novel.ioc.metadata.BeanMetaData;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.jar.JarFile;

/**
 * @author xuhainan
 * @date 2022/8/12 09:29
 * @region hefei
 */
public class ComponentFactory {

    private static final char PACKAGE_SEPARATOR = '.';

    /**
     * The path separator character: {@code '/'}.
     */
    private static final char PATH_SEPARATOR = '/';
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
    public static final String URL_PROTOCOL_FILE = "file";

    public static final String URL_PROTOCOL_JAR = "jar";
    public static final String FILE_URL_PREFIX = "file:";
    public static final String JAR_URL_SEPARATOR = "!/";
    public static final String JAR_FILE_EXTENSION = ".jar";
    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";


    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    private final Map<String, BeanMetaData> beanDefinitionMap = new ConcurrentHashMap<>(256);

    private final List<String> beanDefinitionNames = new ArrayList<>(256);

    final String DEFAULT_PACKAGE = "top.novel.strategy";
    private String scanPackage;


    public static void main(String[] args) {
        new ComponentFactory();
    }

    public ComponentFactory() {
        try {
            scanComponents(DEFAULT_PACKAGE);
        } catch (IOException | URISyntaxException e) {
            throw new RuntimeException("scanPackage " + DEFAULT_PACKAGE + "fail");
        }
    }

    public ComponentFactory(String scanPackage) {
        try {
            scanComponents(scanPackage);
        } catch (IOException | URISyntaxException e) {
            throw new RuntimeException("scanPackage " + scanPackage + "fail");
        }
    }

    public String getScanPackage() {
        return scanPackage;
    }

    public void setScanPackage(String scanPackage) {
        this.scanPackage = scanPackage;
    }


    @SuppressWarnings("unchecked")
    public <T> T getBean(Class<T> requireType) {
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = getBean(beanDefinitionName);
            if (bean.getClass().isAssignableFrom(requireType)) {
                return (T) bean;
            }
        }
        throw new RuntimeException("No qualifying bean of type '" + requireType + "' available");
    }

    public <T> T getBean(String name) {
        try {
            return doGetBean(name);
        } catch (Exception e) {
            System.out.println("创建bean异常");
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T doGetBean(String name) throws Exception {
        Object singleton = getSingleton(name);
        if (singleton == null) {
            singleton = doCreatBean(name);
        }
        return (T) singleton;
    }

    private Object doCreatBean(String name) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        BeanMetaData beanMetaData = beanDefinitionMap.get(name);
        File file = beanMetaData.getFile();
        Class<?> loadClass = this.getClass().getClassLoader().loadClass(file.getAbsolutePath());
        Object bean = ReflectUtil.newInstance(loadClass);
        singletonObjects.put(name, bean);
        beanDefinitionNames.add(name);
        return bean;
    }

    protected Object getSingleton(String beanName) {
        // Quick check for existing instance without full singleton lock
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            synchronized (this.singletonObjects) {
                // Consistent creation of early reference within full singleton lock
                singletonObject = this.singletonObjects.get(beanName);
            }
        }
        return singletonObject;
    }

    public Set<BeanMetaData> scanComponents(String basePackage) throws IOException, URISyntaxException {
        String packageSearchPath = CLASSPATH_ALL_URL_PREFIX + convertClassNameToResourcePath(basePackage) + '/' + DEFAULT_RESOURCE_PATTERN;
        String rootDirPath = determineRootDir(packageSearchPath);
        Set<BeanMetaData> metaDataSet = findAllClassPathResources(rootDirPath.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        metaDataSet.forEach(metaData -> beanDefinitionMap.put(metaData.getName(), metaData));
        return metaDataSet;
    }

    public static URI toURI(String location) throws URISyntaxException {
        return new URI(StrUtil.replace(location, " ", "%20"));
    }

    protected Set<BeanMetaData> findAllClassPathResources(String location) throws IOException, URISyntaxException {
        String path = location;
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        Set<URL> result = doFindAllClassPathResources(path);

        Set<BeanMetaData> metaDataSet = new LinkedHashSet<>();
        for (URL url : result) {
            if (isJarFileURL(url)) {
                url = extractJarFileURL(url);
            }
            File file = new File(toURI(url.toString()).getSchemeSpecificPart());
            getAllData(metaDataSet::add, file);
        }
        return metaDataSet;
    }

    public static boolean isJarFileURL(URL url) {
        if ((URL_PROTOCOL_JAR.equals(url.getProtocol()))) {
            String path = url.getPath();
            int separatorIndex = path.indexOf(JAR_URL_SEPARATOR);
            if (separatorIndex != -1) {
                String jarFile = path.substring(0, separatorIndex);
                return jarFile.toLowerCase().endsWith(JAR_FILE_EXTENSION);
            }
        }
        return false;
    }


    private void getAllData(Consumer<BeanMetaData> metaDataSet, File file) {
        if (file.isFile()) {
            BeanMetaData beanMetaData = new BeanMetaData();
            beanMetaData.setFile(file);
            metaDataSet.accept(beanMetaData);
        }
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null && files.length != 0) {
                Arrays.stream(files).forEach(f -> getAllData(metaDataSet, f));
            }
        }
    }

    protected Set<URL> doFindAllClassPathResources(String path) throws IOException {
        Set<URL> result = new LinkedHashSet<>(16);
        Enumeration<URL> resourceUrls = this.getClass().getClassLoader().getResources(path);
        while (resourceUrls.hasMoreElements()) {
            URL url = resourceUrls.nextElement();
            result.add(url);
        }
        return result;
    }

    public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException {
        String urlFile = jarUrl.getFile();
        int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR);
        if (separatorIndex != -1) {
            String jarFile = urlFile.substring(0, separatorIndex);
            try {
                return new URL(jarFile);
            } catch (MalformedURLException ex) {
                // Probably no protocol in original jar URL, like "jar:C:/mypath/myjar.jar".
                // This usually indicates that the jar file resides in the file system.
                if (!jarFile.startsWith("/")) {
                    jarFile = "/" + jarFile;
                }
                return new URL(FILE_URL_PREFIX + jarFile);
            }
        } else {
            return jarUrl;
        }
    }

    public static String convertClassNameToResourcePath(String className) {
        return className.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR);
    }

    public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
        /*try {
            cl = Thread.currentThread().getContextClassLoader();
        } catch (Throwable ex) {
            // Cannot access thread context ClassLoader - falling back...
        }*/
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = ComponentFactory.class.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                } catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }
        return cl;
    }

    protected String determineRootDir(String location) {
        int prefixEnd = location.indexOf(':') + 1;
        int rootDirEnd = location.length();
        while (rootDirEnd > prefixEnd && isPattern(location.substring(prefixEnd, rootDirEnd))) {
            rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
        }
        if (rootDirEnd == 0) {
            rootDirEnd = prefixEnd;
        }
        return location.substring(0, rootDirEnd);
    }

    public boolean isPattern(String path) {
        if (path == null) {
            return false;
        }
        boolean uriVar = false;
        for (int i = 0; i < path.length(); i++) {
            char c = path.charAt(i);
            if (c == '*' || c == '?') {
                return true;
            }
            if (c == '{') {
                uriVar = true;
                continue;
            }
            if (c == '}' && uriVar) {
                return true;
            }
        }
        return false;
    }
}
